Skip to content

feat(control): show command limits and measured response tiers - #1474

Merged
frahlg merged 19 commits into
masterfrom
feat/control-feedback
Oct 1, 2026
Merged

frahlg merged 19 commits into
masterfrom
feat/control-feedback

Conversation

@frahlg

@frahlg frahlg commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Problem and result

FTW sent commands without showing whether a device did what it asked. Core now answers "Are we in control?" for each controlled device function (battery, charger, solar cap, V2X) in every mode, and a tap on a device shows that answer first.

Core decides the status and how urgent it is; clients only choose words and colours:

Status Meaning Overview mark
Following FTW Fresh readings show the device doing what FTW asked none
Waiting A response is still due, or there is nothing to verify yet none
Limited The device follows within a known limit: battery nearly full, main fuse, charger setting none; amber when the owner can act
Not following Fresh readings disagree with the command after the response time amber
No contact Readings or commands fail; losing a device FTW had measured is an alarm amber, red for the alarm
Not controlled FTW only reads the device or has handed control back none

The evidence is a receipt behind the answer, not the headline: sent, accepted, measured and confirmed (a separate grid meter saw the matching change). The API carries status, severity (info, warning, alarm), evidence (none, accepted, measured, confirmed), reason, readings_fresh, confirmed_at_ms and site_evidence. Support reports include the same rows.

What the 1 October review changed

The review replayed site traces through telemetry, command evidence and the API on a fake clock. The first version restarted proof whenever a command moved by 100 W and assumed one steady command at a time. These gaps are fixed and each has a trace test:

Trace Before Now
Self-consumption, battery follows every command "Waiting" in 62% of samples, 43 status changes in 15 min Following in 96%
Battery stops following in self-consumption No warning in 10 min Warning after 30 s, held
Cloud charger following at a 60 s cadence Never measured Measured and following
Steady Easee charge, power unchanged for minutes Red "measurements lost" Stays measured; the driver confirms the value
Kettle after a confirmed step Confirmation lost Stays confirmed
Charge taper above 96% Amber warning "Battery nearly full", information
Finished car still plugged in "A state this app does not recognise" "Car is full"

How: retuning continues one measurement record and compares each reading with the commands that could still be in force during the response time; only a material step (500 W or the tolerance) starts a new comparison. A source's power_max_age_s sets its freshness everywhere. A change-only source may set control_power_confirmed. The site comparison freezes once its post-step window is complete. A charge shortfall at 90% SoC or more, or a discharge shortfall at 10% or less, is information.

The device sheet now opens with the answer, one sentence and the next step; "How FTW knows" folds the receipt, numbers and curve, and manual override folds below. The overview draws a mark only for warnings and alarms. Tier numbers are gone from the interface. Six reason texts that Core never sent are removed, and a test fails when Core can send a reason without words. The VISION.md section is rewritten at principle level, and the roadmap table is whole again.

Scope and limits

  • The full-battery stop stays: dispatch sends 0 W charge after a fresh 100% reading and allows charge again at 99% or lower. Discharge and fuse relief stay available.
  • Thresholds need more site traces: 500 W material step, 15 s response delay (two minutes for chargers), 90% and 10% SoC for expected tapering, 150 W or 15% site tolerance.
  • Confirmed traces exist only from the home box (Sungrow with its relayed meter). Pixii's signs are not verified on hardware.
  • Evidence lives in memory; it is not durable alarm history or a push notification.

Validation

  • make verify passed at 6716098 (vet, all Go packages, build). api, telemetry and drivers passed again at b490ea2 with the new driver pin.
  • Web: 633 of 633 node tests.
  • Browser review against live home-box data through a read-only proxy, plus fixture states for following and confirmed, not following, lost control, nearly full, charger limit, car full and safety pause, on desktop and at 390 px.

Paired with srcfl/ftw-webapp#75 (9729a9f) and srcfl/device-drivers#149 (d74ace6, pinned here). Registry and design tokens are unchanged.

Checklist

  • The change follows VISION.md and one selected scope.
  • Overlapping PRs and shared contracts were checked.
  • Relevant checks cover changed behaviour and failure paths.
  • A human reviewed changed UI in a browser.
  • A Changeset is included.
  • Every commit has a DCO sign-off.

🤖 Generated with Claude Code

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
frahlg and others added 9 commits September 30, 2026 07:49
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Control proof restarted on every command that differed by 100 W, so modes
that retune the battery each tick showed "waiting" most of the time and never
warned when the battery stopped following. A cloud charger never reached
measured proof, a kettle after a confirmed step removed the confirmation, and
a battery tapering near full raised a warning.

Retuning now continues one measurement record. Each reading is compared with
the commands that could still be in force during the device's response time;
only a material step (500 W or the tolerance) starts a new comparison. A
source's declared power_max_age_s sets its freshness everywhere. The site
comparison freezes once its post-step window is complete. A charge shortfall
at 90% SoC or more, or a discharge shortfall at 10% or less, is information.

Core now states the answer: status (following, waiting, limited,
not_following, no_contact, not_controlled), severity (info, warning, alarm),
evidence (accepted, measured, confirmed), confirmed_at_ms and readings_fresh.
Lost measured proof becomes readings_lost. Site numbers live in
site_evidence and are omitted when not computed. Trace tests replay
self-consumption, an ignored battery, a kettle, a taper and a cloud charger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
A tap on a bubble opened the manual hold form with "Charge 3000 W"
preselected, and the answer sat folded below it in tier labels. The sheet
now starts with "Are we in control?": one status, one sentence with the
measured numbers and, when the owner can act, the next step. "How FTW knows"
lists sent, accepted, measured and confirmed evidence, with numbers and the
response curve folded inside. Manual override folds below; Stop sits in the
active-hold banner.

The overview stays quiet while FTW is in control. Only Core's warning and
alarm draw a mark, an amber triangle or a red disc, and a combined bubble
keeps the worst. The Values view lists each controlled device with its
answer. Words come from Core's status, severity and reason; a test fails
when Core can emit a reason without words. The EV status line keeps power
first. The shared module holds no DOM code; the box renderer lives in
control-feedback-view.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Replace the tier ladder and bubble layout rules in VISION.md with the
status the owner sees and the receipt behind it. Keep the paragraph about
battery learning, but after the roadmap table so the Later row stays in it.
The changeset now describes the shipped behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
The app never shows a bare minus sign. Device, grid and unexplained changes
now read "toward discharge", "toward export" or "more drawn".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 1, 2026
Paired with srcfl/ftw#1474 at 6716098a. The box now states status,
severity and evidence; the app renders them instead of deriving its own
tone from reasons, so a stale grid meter no longer paints every bubble red
and a finished car reads "Car is full" instead of an unknown state.

The panels open with the answer, one sentence and the next step, and keep
"How FTW knows" folded with an evidence receipt, numbers and curves. Marks
appear only for warning (amber) and alarm (red). The charging status line
keeps power first; a warning no longer replaces it.

The control words are now vendored from the box under src/vendor/ftw with
a provenance header and recorded digest, like the flow component, so drift
fails the vendored test. The box's DOM renderer no longer ships here: the
entry bundle is 82.6 kB gzip, down from 85.1 kB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Easee's cloud records power only when it changes; a steady charge kept an
old source time, so measured proof lapsed and the lost-control alarm fired
during normal charging. A driver may now set control_power_confirmed when
the source still hears from the device and the value is unchanged. Core
treats that reading as a measurement at the time it arrived: it keeps a
steady charge measured, and a charger that holds its old power after a new
target now reads as not following instead of waiting forever. Sources that
do not confirm keep the old rule: a repeated sample adds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
frahlg added a commit to srcfl/device-drivers that referenced this pull request Oct 1, 2026
Paired with srcfl/ftw#1474, which now reads control_power_confirmed.

Easee's cloud records power only on change, so a steady charge kept an old
source time and Core raised a lost-control alarm during normal charging.
The driver now marks the unchanged value confirmed while the cloud still
hears from the charger; the source time stays for distinct samples.

Pixii's AC power (40083) is SunSpec generator frame, like its setpoint, so
control_power_w is now negated into site signs. A following battery read as
the wrong direction before. Hardware has not verified either sign yet.

Sungrow claims external_meter only when the meter reads power or phase
current; a meterless install reads zero and claims none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
frahlg and others added 2 commits October 1, 2026 11:36
Moves the bundled snapshot to srcfl/device-drivers#149 at d74ace6a: Easee
marks a steady value confirmed while the cloud hears from the charger,
Pixii's control power leaves SunSpec's generator frame, and Sungrow claims
a separate meter only when one reads power or phase current.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Keep SiteMeasurementSources beside master's haOwner.Bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
@frahlg
frahlg marked this pull request as ready for review October 1, 2026 14:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T14:38:31.328370Z 857fc9a Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 857fc9aa72

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +125 to +126
if len(c.Recent) > 8 {
c.Recent = c.Recent[len(c.Recent)-8:]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain commands for the full response window

With the default two-second control loop, limiting Recent to eight commands preserves only about 14 seconds of targets, while ResponseDelay("ev") explicitly allows a charger two minutes to respond. For a surplus-controlled charger whose target is retuned by small amounts each tick, a fresh reading can therefore match a command that is still legitimately in force but has already been discarded; commandGap then records a persistent mismatch and the dashboard eventually reports a correctly following charger as not_following. Retain history by response-window age, rather than a fixed entry count.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

Comment on lines +290 to +291
if kind == DerPV {
readbackMismatch = math.Abs(math.Abs(*d.SetpointW)-math.Abs(*c.PowerW)) > math.Max(100, math.Abs(*c.PowerW)*0.05)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare PV readback against delayed commands

When a PV ceiling is continuously retuned and the inverter reports the applied setpoint with normal delay, this PV-only override ignores the response-aware commandGap calculated immediately above and compares the readback solely with the newest command. Even if the inverter follows every command within the declared 15-second response window, the mismatch timer can remain set for 30 seconds and surface a false setpoint_changed warning. Apply the same in-force-command window while preserving PV's absolute-sign comparison.

AGENTS.md reference: AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

frahlg added a commit to srcfl/device-drivers that referenced this pull request Oct 1, 2026
* fix(drivers): expose current limits and measured control response

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* fix(easee): retain power source time for control evidence

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Report Sungrow setpoint and external meter evidence

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* fix(drivers): keep steady Easee power current and fix control signs

Paired with srcfl/ftw#1474, which now reads control_power_confirmed.

Easee's cloud records power only on change, so a steady charge kept an old
source time and Core raised a lost-control alarm during normal charging.
The driver now marks the unchanged value confirmed while the cloud still
hears from the charger; the source time stays for distinct samples.

Pixii's AC power (40083) is SunSpec generator frame, like its setpoint, so
control_power_w is now negated into site signs. A following battery read as
the wrong direction before. Hardware has not verified either sign yet.

Sungrow claims external_meter only when the meter reads power or phase
current; a meterless install reads zero and claims none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

---------

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@frahlg
frahlg merged commit 8386721 into master Oct 1, 2026
15 checks passed
frahlg added a commit to srcfl/ftw-webapp that referenced this pull request Oct 1, 2026
…75)

* feat(control): show limits and measured response in Now and charging

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* feat(control): show measured response curves and unexplained changes

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* fix(control): distinguish measurement sources from active control

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Show measured timing in milliseconds and name missing sources

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Show per-device proof status and verification-loss alarms

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Keep per-device control proof in overview bubbles and a detail dialog

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Use small quiet control indicators and keep full evidence on tap

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Open device panels directly and fold evidence below controls

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Show confirmed partial power separately from target shortfall

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Explain the full-battery charging pause

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

* Answer "Are we in control?" first, with the box's words

Paired with srcfl/ftw#1474 at 6716098a. The box now states status,
severity and evidence; the app renders them instead of deriving its own
tone from reasons, so a stale grid meter no longer paints every bubble red
and a finished car reads "Car is full" instead of an unknown state.

The panels open with the answer, one sentence and the next step, and keep
"How FTW knows" folded with an evidence receipt, numbers and curves. Marks
appear only for warning (amber) and alarm (red). The charging status line
keeps power first; a warning no longer replaces it.

The control words are now vendored from the box under src/vendor/ftw with
a provenance header and recorded digest, like the flow component, so drift
fails the vendored test. The box's DOM renderer no longer ships here: the
entry bundle is 82.6 kB gzip, down from 85.1 kB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>

---------

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
frahlg added a commit that referenced this pull request Oct 1, 2026
#1474 pinned d74ace6, a commit on the #149 branch. #149 has now
landed on device-drivers main as 424f1e5c. Moving the pin there keeps the
bundle on main and brings esphome_dsmr 1.0.7 (srcfl/device-drivers#152),
the only bundled driver that differs.

Signed-off-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant